/*
 * ====================================================================
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 * ====================================================================
 *
 * This software consists of voluntary contributions made by many
 * individuals on behalf of the Apache Software Foundation.  For more
 * information on the Apache Software Foundation, please see
 * <http://www.apache.org/>.
 *
 */
package com.feilong.lib.org.apache.http.impl.auth;

import java.nio.charset.Charset;
import java.security.Key;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.cert.Certificate;
import java.security.cert.CertificateEncodingException;
import java.util.Arrays;
import java.util.Locale;
import java.util.Random;

import javax.crypto.Cipher;
import javax.crypto.spec.SecretKeySpec;

import com.feilong.lib.codec.binary.Base64;
import com.feilong.lib.org.apache.http.Consts;

/**
 * Provides an implementation for NTLMv1, NTLMv2, and NTLM2 Session forms of the NTLM
 * authentication protocol.
 *
 * @since 4.1
 */
final class NTLMEngineImpl implements NTLMEngine{

    /** Unicode encoding */
    private static final Charset                    UNICODE_LITTLE_UNMARKED              = Charset.forName("UnicodeLittleUnmarked");

    /** Character encoding */
    private static final Charset                    DEFAULT_CHARSET                      = Consts.ASCII;

    // Flags we use; descriptions according to:
    // http://davenport.sourceforge.net/ntlm.html
    // and
    // http://msdn.microsoft.com/en-us/library/cc236650%28v=prot.20%29.aspx
    // [MS-NLMP] section 2.2.2.5
    static final int                                FLAG_REQUEST_UNICODE_ENCODING        = 0x00000001;                              // Unicode string encoding requested

    static final int                                FLAG_REQUEST_OEM_ENCODING            = 0x00000002;                              // OEM string encoding requested

    static final int                                FLAG_REQUEST_TARGET                  = 0x00000004;                              // Requests target field

    static final int                                FLAG_REQUEST_SIGN                    = 0x00000010;                              // Requests all messages have a signature attached, in NEGOTIATE message.

    static final int                                FLAG_REQUEST_SEAL                    = 0x00000020;                              // Request key exchange for message confidentiality in NEGOTIATE message.  MUST be used in conjunction with 56BIT.

    static final int                                FLAG_REQUEST_LAN_MANAGER_KEY         = 0x00000080;                              // Request Lan Manager key instead of user session key

    static final int                                FLAG_REQUEST_NTLMv1                  = 0x00000200;                              // Request NTLMv1 security.  MUST be set in NEGOTIATE and CHALLENGE both

    static final int                                FLAG_DOMAIN_PRESENT                  = 0x00001000;                              // Domain is present in message

    static final int                                FLAG_WORKSTATION_PRESENT             = 0x00002000;                              // Workstation is present in message

    static final int                                FLAG_REQUEST_ALWAYS_SIGN             = 0x00008000;                              // Requests a signature block on all messages.  Overridden by REQUEST_SIGN and REQUEST_SEAL.

    static final int                                FLAG_REQUEST_NTLM2_SESSION           = 0x00080000;                              // From server in challenge, requesting NTLM2 session security

    static final int                                FLAG_REQUEST_VERSION                 = 0x02000000;                              // Request protocol version

    static final int                                FLAG_TARGETINFO_PRESENT              = 0x00800000;                              // From server in challenge message, indicating targetinfo is present

    static final int                                FLAG_REQUEST_128BIT_KEY_EXCH         = 0x20000000;                              // Request explicit 128-bit key exchange

    static final int                                FLAG_REQUEST_EXPLICIT_KEY_EXCH       = 0x40000000;                              // Request explicit key exchange

    static final int                                FLAG_REQUEST_56BIT_ENCRYPTION        = 0x80000000;                              // Must be used in conjunction with SEAL

    // Attribute-value identifiers (AvId)
    // according to [MS-NLMP] section 2.2.2.1
    static final int                                MSV_AV_EOL                           = 0x0000;                                  // Indicates that this is the last AV_PAIR in the list.

    static final int                                MSV_AV_NB_COMPUTER_NAME              = 0x0001;                                  // The server's NetBIOS computer name.

    static final int                                MSV_AV_NB_DOMAIN_NAME                = 0x0002;                                  // The server's NetBIOS domain name.

    static final int                                MSV_AV_DNS_COMPUTER_NAME             = 0x0003;                                  // The fully qualified domain name (FQDN) of the computer.

    static final int                                MSV_AV_DNS_DOMAIN_NAME               = 0x0004;                                  // The FQDN of the domain.

    static final int                                MSV_AV_DNS_TREE_NAME                 = 0x0005;                                  // The FQDN of the forest.

    static final int                                MSV_AV_FLAGS                         = 0x0006;                                  // A 32-bit value indicating server or client configuration.

    static final int                                MSV_AV_TIMESTAMP                     = 0x0007;                                  // server local time

    static final int                                MSV_AV_SINGLE_HOST                   = 0x0008;                                  // A Single_Host_Data structure.

    static final int                                MSV_AV_TARGET_NAME                   = 0x0009;                                  // The SPN of the target server.

    static final int                                MSV_AV_CHANNEL_BINDINGS              = 0x000A;                                  // A channel bindings hash.

    static final int                                MSV_AV_FLAGS_ACCOUNT_AUTH_CONSTAINED = 0x00000001;                              // Indicates to the client that the account authentication is constrained.

    static final int                                MSV_AV_FLAGS_MIC                     = 0x00000002;                              // Indicates that the client is providing message integrity in the MIC field in the AUTHENTICATE_MESSAGE.

    static final int                                MSV_AV_FLAGS_UNTRUSTED_TARGET_SPN    = 0x00000004;                              // Indicates that the client is providing a target SPN generated from an untrusted source.

    /** Secure random generator */
    private static final java.security.SecureRandom RND_GEN;
    static{
        java.security.SecureRandom rnd = null;
        try{
            rnd = java.security.SecureRandom.getInstance("SHA1PRNG");
        }catch (final Exception ignore){}
        RND_GEN = rnd;
    }

    /** The signature string as bytes in the default encoding */
    private static final byte[] SIGNATURE                 = getNullTerminatedAsciiString("NTLMSSP");

    // Key derivation magic strings for the SIGNKEY algorithm defined in
    // [MS-NLMP] section 3.4.5.2
    private static final byte[] SIGN_MAGIC_SERVER         = getNullTerminatedAsciiString(
                    "session key to server-to-client signing key magic constant");

    private static final byte[] SIGN_MAGIC_CLIENT         = getNullTerminatedAsciiString(
                    "session key to client-to-server signing key magic constant");

    private static final byte[] SEAL_MAGIC_SERVER         = getNullTerminatedAsciiString(
                    "session key to server-to-client sealing key magic constant");

    private static final byte[] SEAL_MAGIC_CLIENT         = getNullTerminatedAsciiString(
                    "session key to client-to-server sealing key magic constant");

    // prefix for GSS API channel binding
    private static final byte[] MAGIC_TLS_SERVER_ENDPOINT = "tls-server-end-point:".getBytes(Consts.ASCII);

    private static byte[] getNullTerminatedAsciiString(final String source){
        final byte[] bytesWithoutNull = source.getBytes(Consts.ASCII);
        final byte[] target = new byte[bytesWithoutNull.length + 1];
        System.arraycopy(bytesWithoutNull, 0, target, 0, bytesWithoutNull.length);
        target[bytesWithoutNull.length] = (byte) 0x00;
        return target;
    }

    private static final String TYPE_1_MESSAGE = new Type1Message().getResponse();

    NTLMEngineImpl(){
    }

    /**
     * Creates the first message (type 1 message) in the NTLM authentication
     * sequence. This message includes the user name, domain and host for the
     * authentication session.
     *
     * @param host
     *            the computer name of the host requesting authentication.
     * @param domain
     *            The domain to authenticate with.
     * @return String the message to add to the HTTP request header.
     */
    static String getType1Message(final String host,final String domain){
        // For compatibility reason do not include domain and host in type 1 message
        //return new Type1Message(domain, host).getResponse();
        return TYPE_1_MESSAGE;
    }

    /**
     * Creates the type 3 message using the given server nonce. The type 3
     * message includes all the information for authentication, host, domain,
     * username and the result of encrypting the nonce sent by the server using
     * the user's password as the key.
     *
     * @param user
     *            The user name. This should not include the domain name.
     * @param password
     *            The password.
     * @param host
     *            The host that is originating the authentication request.
     * @param domain
     *            The domain to authenticate within.
     * @param nonce
     *            the 8 byte array the server sent.
     * @return The type 3 message.
     * @throws NTLMEngineException
     *             If {@link #Type3Message
     *             (String, String, String, String, byte[], int, String, byte[])} fails.
     */
    static String getType3Message(
                    final String user,
                    final String password,
                    final String host,
                    final String domain,
                    final byte[] nonce,
                    final int type2Flags,
                    final String target,
                    final byte[] targetInformation) throws NTLMEngineException{
        return new Type3Message(domain, host, user, password, nonce, type2Flags, target, targetInformation).getResponse();
    }

    /**
     * Creates the type 3 message using the given server nonce. The type 3
     * message includes all the information for authentication, host, domain,
     * username and the result of encrypting the nonce sent by the server using
     * the user's password as the key.
     *
     * @param user
     *            The user name. This should not include the domain name.
     * @param password
     *            The password.
     * @param host
     *            The host that is originating the authentication request.
     * @param domain
     *            The domain to authenticate within.
     * @param nonce
     *            the 8 byte array the server sent.
     * @return The type 3 message.
     * @throws NTLMEngineException
     *             If {@link #Type3Message
     *             (String, String, String, String, byte[], int, String, byte[], Certificate, byte[], byte[])} fails.
     */
    static String getType3Message(
                    final String user,
                    final String password,
                    final String host,
                    final String domain,
                    final byte[] nonce,
                    final int type2Flags,
                    final String target,
                    final byte[] targetInformation,
                    final Certificate peerServerCertificate,
                    final byte[] type1Message,
                    final byte[] type2Message) throws NTLMEngineException{
        return new Type3Message(
                        domain,
                        host,
                        user,
                        password,
                        nonce,
                        type2Flags,
                        target,
                        targetInformation,
                        peerServerCertificate,
                        type1Message,
                        type2Message).getResponse();
    }

    private static int readULong(final byte[] src,final int index){
        if (src.length < index + 4){
            return 0;
        }
        return (src[index] & 0xff) | ((src[index + 1] & 0xff) << 8) | ((src[index + 2] & 0xff) << 16) | ((src[index + 3] & 0xff) << 24);
    }

    private static int readUShort(final byte[] src,final int index){
        if (src.length < index + 2){
            return 0;
        }
        return (src[index] & 0xff) | ((src[index + 1] & 0xff) << 8);
    }

    private static byte[] readSecurityBuffer(final byte[] src,final int index){
        final int length = readUShort(src, index);
        final int offset = readULong(src, index + 4);
        if (src.length < offset + length){
            return new byte[length];
        }
        final byte[] buffer = new byte[length];
        System.arraycopy(src, offset, buffer, 0, length);
        return buffer;
    }

    /** Calculate a challenge block */
    private static byte[] makeRandomChallenge(final Random random){
        final byte[] rval = new byte[8];
        synchronized (random){
            random.nextBytes(rval);
        }
        return rval;
    }

    /** Calculate a 16-byte secondary key */
    private static byte[] makeSecondaryKey(final Random random){
        final byte[] rval = new byte[16];
        synchronized (random){
            random.nextBytes(rval);
        }
        return rval;
    }

    protected static class CipherGen{

        protected final Random random;

        protected final long   currentTime;

        protected final String domain;

        protected final String user;

        protected final String password;

        protected final byte[] challenge;

        protected final String target;

        protected final byte[] targetInformation;

        // Information we can generate but may be passed in (for testing)
        protected byte[]       clientChallenge;

        protected byte[]       clientChallenge2;

        protected byte[]       secondaryKey;

        protected byte[]       timestamp;

        // Stuff we always generate
        protected byte[]       lmHash                             = null;

        protected byte[]       lmResponse                         = null;

        protected byte[]       ntlmHash                           = null;

        protected byte[]       ntlmResponse                       = null;

        protected byte[]       ntlmv2Hash                         = null;

        protected byte[]       lmv2Hash                           = null;

        protected byte[]       lmv2Response                       = null;

        protected byte[]       ntlmv2Blob                         = null;

        protected byte[]       ntlmv2Response                     = null;

        protected byte[]       ntlm2SessionResponse               = null;

        protected byte[]       lm2SessionResponse                 = null;

        protected byte[]       lmUserSessionKey                   = null;

        protected byte[]       ntlmUserSessionKey                 = null;

        protected byte[]       ntlmv2UserSessionKey               = null;

        protected byte[]       ntlm2SessionResponseUserSessionKey = null;

        protected byte[]       lanManagerSessionKey               = null;

        /**
         * @deprecated Use
         *             {@link CipherGen#CipherGen(Random, long, String, String, String, byte[], String, byte[], byte[], byte[], byte[], byte[])}
         */
        @Deprecated
        public CipherGen(final String domain, final String user, final String password, final byte[] challenge, final String target,
                        final byte[] targetInformation, final byte[] clientChallenge, final byte[] clientChallenge2,
                        final byte[] secondaryKey, final byte[] timestamp){
            this(RND_GEN, System.currentTimeMillis(), domain, user, password, challenge, target, targetInformation, clientChallenge,
                            clientChallenge2, secondaryKey, timestamp);
        }

        public CipherGen(final Random random, final long currentTime, final String domain, final String user, final String password,
                        final byte[] challenge, final String target, final byte[] targetInformation, final byte[] clientChallenge,
                        final byte[] clientChallenge2, final byte[] secondaryKey, final byte[] timestamp){
            this.random = random;
            this.currentTime = currentTime;

            this.domain = domain;
            this.target = target;
            this.user = user;
            this.password = password;
            this.challenge = challenge;
            this.targetInformation = targetInformation;
            this.clientChallenge = clientChallenge;
            this.clientChallenge2 = clientChallenge2;
            this.secondaryKey = secondaryKey;
            this.timestamp = timestamp;
        }

        /**
         * @deprecated Use
         *             {@link CipherGen#CipherGen(Random, long, String, String, String, byte[], String, byte[], byte[], byte[], byte[], byte[])}
         */
        @Deprecated
        public CipherGen(final String domain, final String user, final String password, final byte[] challenge, final String target,
                        final byte[] targetInformation){
            this(RND_GEN, System.currentTimeMillis(), domain, user, password, challenge, target, targetInformation);
        }

        public CipherGen(final Random random, final long currentTime, final String domain, final String user, final String password,
                        final byte[] challenge, final String target, final byte[] targetInformation){
            this(random, currentTime, domain, user, password, challenge, target, targetInformation, null, null, null, null);
        }

        /** Calculate and return client challenge */
        public byte[] getClientChallenge() throws NTLMEngineException{
            if (clientChallenge == null){
                clientChallenge = makeRandomChallenge(random);
            }
            return clientChallenge;
        }

        /** Calculate and return second client challenge */
        public byte[] getClientChallenge2() throws NTLMEngineException{
            if (clientChallenge2 == null){
                clientChallenge2 = makeRandomChallenge(random);
            }
            return clientChallenge2;
        }

        /** Calculate and return random secondary key */
        public byte[] getSecondaryKey() throws NTLMEngineException{
            if (secondaryKey == null){
                secondaryKey = makeSecondaryKey(random);
            }
            return secondaryKey;
        }

        /** Calculate and return the LMHash */
        public byte[] getLMHash() throws NTLMEngineException{
            if (lmHash == null){
                lmHash = lmHash(password);
            }
            return lmHash;
        }

        /** Calculate and return the LMResponse */
        public byte[] getLMResponse() throws NTLMEngineException{
            if (lmResponse == null){
                lmResponse = lmResponse(getLMHash(), challenge);
            }
            return lmResponse;
        }

        /** Calculate and return the NTLMHash */
        public byte[] getNTLMHash() throws NTLMEngineException{
            if (ntlmHash == null){
                ntlmHash = ntlmHash(password);
            }
            return ntlmHash;
        }

        /** Calculate and return the NTLMResponse */
        public byte[] getNTLMResponse() throws NTLMEngineException{
            if (ntlmResponse == null){
                ntlmResponse = lmResponse(getNTLMHash(), challenge);
            }
            return ntlmResponse;
        }

        /** Calculate the LMv2 hash */
        public byte[] getLMv2Hash() throws NTLMEngineException{
            if (lmv2Hash == null){
                lmv2Hash = lmv2Hash(domain, user, getNTLMHash());
            }
            return lmv2Hash;
        }

        /** Calculate the NTLMv2 hash */
        public byte[] getNTLMv2Hash() throws NTLMEngineException{
            if (ntlmv2Hash == null){
                ntlmv2Hash = ntlmv2Hash(domain, user, getNTLMHash());
            }
            return ntlmv2Hash;
        }

        /** Calculate a timestamp */
        public byte[] getTimestamp(){
            if (timestamp == null){
                long time = this.currentTime;
                time += 11644473600000L; // milliseconds from January 1, 1601 -> epoch.
                time *= 10000; // tenths of a microsecond.
                // convert to little-endian byte array.
                timestamp = new byte[8];
                for (int i = 0; i < 8; i++){
                    timestamp[i] = (byte) time;
                    time >>>= 8;
                }
            }
            return timestamp;
        }

        /** Calculate the NTLMv2Blob */
        public byte[] getNTLMv2Blob() throws NTLMEngineException{
            if (ntlmv2Blob == null){
                ntlmv2Blob = createBlob(getClientChallenge2(), targetInformation, getTimestamp());
            }
            return ntlmv2Blob;
        }

        /** Calculate the NTLMv2Response */
        public byte[] getNTLMv2Response() throws NTLMEngineException{
            if (ntlmv2Response == null){
                ntlmv2Response = lmv2Response(getNTLMv2Hash(), challenge, getNTLMv2Blob());
            }
            return ntlmv2Response;
        }

        /** Calculate the LMv2Response */
        public byte[] getLMv2Response() throws NTLMEngineException{
            if (lmv2Response == null){
                lmv2Response = lmv2Response(getLMv2Hash(), challenge, getClientChallenge());
            }
            return lmv2Response;
        }

        /** Get NTLM2SessionResponse */
        public byte[] getNTLM2SessionResponse() throws NTLMEngineException{
            if (ntlm2SessionResponse == null){
                ntlm2SessionResponse = ntlm2SessionResponse(getNTLMHash(), challenge, getClientChallenge());
            }
            return ntlm2SessionResponse;
        }

        /** Calculate and return LM2 session response */
        public byte[] getLM2SessionResponse() throws NTLMEngineException{
            if (lm2SessionResponse == null){
                final byte[] clntChallenge = getClientChallenge();
                lm2SessionResponse = new byte[24];
                System.arraycopy(clntChallenge, 0, lm2SessionResponse, 0, clntChallenge.length);
                Arrays.fill(lm2SessionResponse, clntChallenge.length, lm2SessionResponse.length, (byte) 0x00);
            }
            return lm2SessionResponse;
        }

        /** Get LMUserSessionKey */
        public byte[] getLMUserSessionKey() throws NTLMEngineException{
            if (lmUserSessionKey == null){
                lmUserSessionKey = new byte[16];
                System.arraycopy(getLMHash(), 0, lmUserSessionKey, 0, 8);
                Arrays.fill(lmUserSessionKey, 8, 16, (byte) 0x00);
            }
            return lmUserSessionKey;
        }

        /** Get NTLMUserSessionKey */
        public byte[] getNTLMUserSessionKey() throws NTLMEngineException{
            if (ntlmUserSessionKey == null){
                final MD4 md4 = new MD4();
                md4.update(getNTLMHash());
                ntlmUserSessionKey = md4.getOutput();
            }
            return ntlmUserSessionKey;
        }

        /** GetNTLMv2UserSessionKey */
        public byte[] getNTLMv2UserSessionKey() throws NTLMEngineException{
            if (ntlmv2UserSessionKey == null){
                final byte[] ntlmv2hash = getNTLMv2Hash();
                final byte[] truncatedResponse = new byte[16];
                System.arraycopy(getNTLMv2Response(), 0, truncatedResponse, 0, 16);
                ntlmv2UserSessionKey = hmacMD5(truncatedResponse, ntlmv2hash);
            }
            return ntlmv2UserSessionKey;
        }

        /** Get NTLM2SessionResponseUserSessionKey */
        public byte[] getNTLM2SessionResponseUserSessionKey() throws NTLMEngineException{
            if (ntlm2SessionResponseUserSessionKey == null){
                final byte[] ntlm2SessionResponseNonce = getLM2SessionResponse();
                final byte[] sessionNonce = new byte[challenge.length + ntlm2SessionResponseNonce.length];
                System.arraycopy(challenge, 0, sessionNonce, 0, challenge.length);
                System.arraycopy(ntlm2SessionResponseNonce, 0, sessionNonce, challenge.length, ntlm2SessionResponseNonce.length);
                ntlm2SessionResponseUserSessionKey = hmacMD5(sessionNonce, getNTLMUserSessionKey());
            }
            return ntlm2SessionResponseUserSessionKey;
        }

        /** Get LAN Manager session key */
        public byte[] getLanManagerSessionKey() throws NTLMEngineException{
            if (lanManagerSessionKey == null){
                try{
                    final byte[] keyBytes = new byte[14];
                    System.arraycopy(getLMHash(), 0, keyBytes, 0, 8);
                    Arrays.fill(keyBytes, 8, keyBytes.length, (byte) 0xbd);
                    final Key lowKey = createDESKey(keyBytes, 0);
                    final Key highKey = createDESKey(keyBytes, 7);
                    final byte[] truncatedResponse = new byte[8];
                    System.arraycopy(getLMResponse(), 0, truncatedResponse, 0, truncatedResponse.length);
                    Cipher des = Cipher.getInstance("DES/ECB/NoPadding");
                    des.init(Cipher.ENCRYPT_MODE, lowKey);
                    final byte[] lowPart = des.doFinal(truncatedResponse);
                    des = Cipher.getInstance("DES/ECB/NoPadding");
                    des.init(Cipher.ENCRYPT_MODE, highKey);
                    final byte[] highPart = des.doFinal(truncatedResponse);
                    lanManagerSessionKey = new byte[16];
                    System.arraycopy(lowPart, 0, lanManagerSessionKey, 0, lowPart.length);
                    System.arraycopy(highPart, 0, lanManagerSessionKey, lowPart.length, highPart.length);
                }catch (final Exception e){
                    throw new NTLMEngineException(e.getMessage(), e);
                }
            }
            return lanManagerSessionKey;
        }
    }

    /** Calculates HMAC-MD5 */
    static byte[] hmacMD5(final byte[] value,final byte[] key) throws NTLMEngineException{
        final HMACMD5 hmacMD5 = new HMACMD5(key);
        hmacMD5.update(value);
        return hmacMD5.getOutput();
    }

    /** Calculates RC4 */
    static byte[] RC4(final byte[] value,final byte[] key) throws NTLMEngineException{
        try{
            final Cipher rc4 = Cipher.getInstance("RC4");
            rc4.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(key, "RC4"));
            return rc4.doFinal(value);
        }catch (final Exception e){
            throw new NTLMEngineException(e.getMessage(), e);
        }
    }

    /**
     * Calculates the NTLM2 Session Response for the given challenge, using the
     * specified password and client challenge.
     *
     * @return The NTLM2 Session Response. This is placed in the NTLM response
     *         field of the Type 3 message; the LM response field contains the
     *         client challenge, null-padded to 24 bytes.
     */
    static byte[] ntlm2SessionResponse(final byte[] ntlmHash,final byte[] challenge,final byte[] clientChallenge)
                    throws NTLMEngineException{
        try{
            final MessageDigest md5 = getMD5();
            md5.update(challenge);
            md5.update(clientChallenge);
            final byte[] digest = md5.digest();

            final byte[] sessionHash = new byte[8];
            System.arraycopy(digest, 0, sessionHash, 0, 8);
            return lmResponse(ntlmHash, sessionHash);
        }catch (final Exception e){
            if (e instanceof NTLMEngineException){
                throw (NTLMEngineException) e;
            }
            throw new NTLMEngineException(e.getMessage(), e);
        }
    }

    /**
     * Creates the LM Hash of the user's password.
     *
     * @param password
     *            The password.
     *
     * @return The LM Hash of the given password, used in the calculation of the
     *         LM Response.
     */
    private static byte[] lmHash(final String password) throws NTLMEngineException{
        try{
            final byte[] oemPassword = password.toUpperCase(Locale.ROOT).getBytes(Consts.ASCII);
            final int length = Math.min(oemPassword.length, 14);
            final byte[] keyBytes = new byte[14];
            System.arraycopy(oemPassword, 0, keyBytes, 0, length);
            final Key lowKey = createDESKey(keyBytes, 0);
            final Key highKey = createDESKey(keyBytes, 7);
            final byte[] magicConstant = "KGS!@#$%".getBytes(Consts.ASCII);
            final Cipher des = Cipher.getInstance("DES/ECB/NoPadding");
            des.init(Cipher.ENCRYPT_MODE, lowKey);
            final byte[] lowHash = des.doFinal(magicConstant);
            des.init(Cipher.ENCRYPT_MODE, highKey);
            final byte[] highHash = des.doFinal(magicConstant);
            final byte[] lmHash = new byte[16];
            System.arraycopy(lowHash, 0, lmHash, 0, 8);
            System.arraycopy(highHash, 0, lmHash, 8, 8);
            return lmHash;
        }catch (final Exception e){
            throw new NTLMEngineException(e.getMessage(), e);
        }
    }

    /**
     * Creates the NTLM Hash of the user's password.
     *
     * @param password
     *            The password.
     *
     * @return The NTLM Hash of the given password, used in the calculation of
     *         the NTLM Response and the NTLMv2 and LMv2 Hashes.
     */
    private static byte[] ntlmHash(final String password) throws NTLMEngineException{
        if (UNICODE_LITTLE_UNMARKED == null){
            throw new NTLMEngineException("Unicode not supported");
        }
        final byte[] unicodePassword = password.getBytes(UNICODE_LITTLE_UNMARKED);
        final MD4 md4 = new MD4();
        md4.update(unicodePassword);
        return md4.getOutput();
    }

    /**
     * Creates the LMv2 Hash of the user's password.
     *
     * @return The LMv2 Hash, used in the calculation of the NTLMv2 and LMv2
     *         Responses.
     */
    private static byte[] lmv2Hash(final String domain,final String user,final byte[] ntlmHash) throws NTLMEngineException{
        if (UNICODE_LITTLE_UNMARKED == null){
            throw new NTLMEngineException("Unicode not supported");
        }
        final HMACMD5 hmacMD5 = new HMACMD5(ntlmHash);
        // Upper case username, upper case domain!
        hmacMD5.update(user.toUpperCase(Locale.ROOT).getBytes(UNICODE_LITTLE_UNMARKED));
        if (domain != null){
            hmacMD5.update(domain.toUpperCase(Locale.ROOT).getBytes(UNICODE_LITTLE_UNMARKED));
        }
        return hmacMD5.getOutput();
    }

    /**
     * Creates the NTLMv2 Hash of the user's password.
     *
     * @return The NTLMv2 Hash, used in the calculation of the NTLMv2 and LMv2
     *         Responses.
     */
    private static byte[] ntlmv2Hash(final String domain,final String user,final byte[] ntlmHash) throws NTLMEngineException{
        if (UNICODE_LITTLE_UNMARKED == null){
            throw new NTLMEngineException("Unicode not supported");
        }
        final HMACMD5 hmacMD5 = new HMACMD5(ntlmHash);
        // Upper case username, mixed case target!!
        hmacMD5.update(user.toUpperCase(Locale.ROOT).getBytes(UNICODE_LITTLE_UNMARKED));
        if (domain != null){
            hmacMD5.update(domain.getBytes(UNICODE_LITTLE_UNMARKED));
        }
        return hmacMD5.getOutput();
    }

    /**
     * Creates the LM Response from the given hash and Type 2 challenge.
     *
     * @param hash
     *            The LM or NTLM Hash.
     * @param challenge
     *            The server challenge from the Type 2 message.
     *
     * @return The response (either LM or NTLM, depending on the provided hash).
     */
    private static byte[] lmResponse(final byte[] hash,final byte[] challenge) throws NTLMEngineException{
        try{
            final byte[] keyBytes = new byte[21];
            System.arraycopy(hash, 0, keyBytes, 0, 16);
            final Key lowKey = createDESKey(keyBytes, 0);
            final Key middleKey = createDESKey(keyBytes, 7);
            final Key highKey = createDESKey(keyBytes, 14);
            final Cipher des = Cipher.getInstance("DES/ECB/NoPadding");
            des.init(Cipher.ENCRYPT_MODE, lowKey);
            final byte[] lowResponse = des.doFinal(challenge);
            des.init(Cipher.ENCRYPT_MODE, middleKey);
            final byte[] middleResponse = des.doFinal(challenge);
            des.init(Cipher.ENCRYPT_MODE, highKey);
            final byte[] highResponse = des.doFinal(challenge);
            final byte[] lmResponse = new byte[24];
            System.arraycopy(lowResponse, 0, lmResponse, 0, 8);
            System.arraycopy(middleResponse, 0, lmResponse, 8, 8);
            System.arraycopy(highResponse, 0, lmResponse, 16, 8);
            return lmResponse;
        }catch (final Exception e){
            throw new NTLMEngineException(e.getMessage(), e);
        }
    }

    /**
     * Creates the LMv2 Response from the given hash, client data, and Type 2
     * challenge.
     *
     * @param hash
     *            The NTLMv2 Hash.
     * @param clientData
     *            The client data (blob or client challenge).
     * @param challenge
     *            The server challenge from the Type 2 message.
     *
     * @return The response (either NTLMv2 or LMv2, depending on the client
     *         data).
     */
    private static byte[] lmv2Response(final byte[] hash,final byte[] challenge,final byte[] clientData){
        final HMACMD5 hmacMD5 = new HMACMD5(hash);
        hmacMD5.update(challenge);
        hmacMD5.update(clientData);
        final byte[] mac = hmacMD5.getOutput();
        final byte[] lmv2Response = new byte[mac.length + clientData.length];
        System.arraycopy(mac, 0, lmv2Response, 0, mac.length);
        System.arraycopy(clientData, 0, lmv2Response, mac.length, clientData.length);
        return lmv2Response;
    }

    enum Mode{
        CLIENT, SERVER;
    }

    static class Handle{

        final private byte[]  exportedSessionKey;

        private byte[]        signingKey;

        private byte[]        sealingKey;

        private final Cipher  rc4;

        final Mode            mode;

        final private boolean isConnection;

        int                   sequenceNumber = 0;

        Handle(final byte[] exportedSessionKey, final Mode mode, final boolean isConnection) throws NTLMEngineException{
            this.exportedSessionKey = exportedSessionKey;
            this.isConnection = isConnection;
            this.mode = mode;
            try{
                final MessageDigest signMd5 = getMD5();
                final MessageDigest sealMd5 = getMD5();
                signMd5.update(exportedSessionKey);
                sealMd5.update(exportedSessionKey);
                if (mode == Mode.CLIENT){
                    signMd5.update(SIGN_MAGIC_CLIENT);
                    sealMd5.update(SEAL_MAGIC_CLIENT);
                }else{
                    signMd5.update(SIGN_MAGIC_SERVER);
                    sealMd5.update(SEAL_MAGIC_SERVER);
                }
                signingKey = signMd5.digest();
                sealingKey = sealMd5.digest();
            }catch (final Exception e){
                throw new NTLMEngineException(e.getMessage(), e);
            }
            rc4 = initCipher();
        }

        public byte[] getSigningKey(){
            return signingKey;
        }

        public byte[] getSealingKey(){
            return sealingKey;
        }

        private Cipher initCipher() throws NTLMEngineException{
            final Cipher cipher;
            try{
                cipher = Cipher.getInstance("RC4");
                if (mode == Mode.CLIENT){
                    cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(sealingKey, "RC4"));
                }else{
                    cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(sealingKey, "RC4"));
                }
            }catch (final Exception e){
                throw new NTLMEngineException(e.getMessage(), e);
            }
            return cipher;
        }

        private void advanceMessageSequence() throws NTLMEngineException{
            if (!isConnection){
                final MessageDigest sealMd5 = getMD5();
                sealMd5.update(sealingKey);
                final byte[] seqNumBytes = new byte[4];
                writeULong(seqNumBytes, sequenceNumber, 0);
                sealMd5.update(seqNumBytes);
                sealingKey = sealMd5.digest();
                initCipher();
            }
            sequenceNumber++;
        }

        private byte[] encrypt(final byte[] data){
            return rc4.update(data);
        }

        private byte[] decrypt(final byte[] data){
            return rc4.update(data);
        }

        private byte[] computeSignature(final byte[] message){
            final byte[] sig = new byte[16];

            // version
            sig[0] = 0x01;
            sig[1] = 0x00;
            sig[2] = 0x00;
            sig[3] = 0x00;

            // HMAC (first 8 bytes)
            final HMACMD5 hmacMD5 = new HMACMD5(signingKey);
            hmacMD5.update(encodeLong(sequenceNumber));
            hmacMD5.update(message);
            final byte[] hmac = hmacMD5.getOutput();
            final byte[] trimmedHmac = new byte[8];
            System.arraycopy(hmac, 0, trimmedHmac, 0, 8);
            final byte[] encryptedHmac = encrypt(trimmedHmac);
            System.arraycopy(encryptedHmac, 0, sig, 4, 8);

            // sequence number
            encodeLong(sig, 12, sequenceNumber);

            return sig;
        }

        private boolean validateSignature(final byte[] signature,final byte message[]){
            final byte[] computedSignature = computeSignature(message);
            //            log.info( "SSSSS validateSignature("+seqNumber+")\n"
            //                + "  received: " + DebugUtil.dump( signature ) + "\n"
            //                + "  computed: " + DebugUtil.dump( computedSignature ) );
            return Arrays.equals(signature, computedSignature);
        }

        public byte[] signAndEncryptMessage(final byte[] cleartextMessage) throws NTLMEngineException{
            final byte[] encryptedMessage = encrypt(cleartextMessage);
            final byte[] signature = computeSignature(cleartextMessage);
            final byte[] outMessage = new byte[signature.length + encryptedMessage.length];
            System.arraycopy(signature, 0, outMessage, 0, signature.length);
            System.arraycopy(encryptedMessage, 0, outMessage, signature.length, encryptedMessage.length);
            advanceMessageSequence();
            return outMessage;
        }

        public byte[] decryptAndVerifySignedMessage(final byte[] inMessage) throws NTLMEngineException{
            final byte[] signature = new byte[16];
            System.arraycopy(inMessage, 0, signature, 0, signature.length);
            final byte[] encryptedMessage = new byte[inMessage.length - 16];
            System.arraycopy(inMessage, 16, encryptedMessage, 0, encryptedMessage.length);
            final byte[] cleartextMessage = decrypt(encryptedMessage);
            if (!validateSignature(signature, cleartextMessage)){
                throw new NTLMEngineException("Wrong signature");
            }
            advanceMessageSequence();
            return cleartextMessage;
        }

    }

    private static byte[] encodeLong(final int value){
        final byte[] enc = new byte[4];
        encodeLong(enc, 0, value);
        return enc;
    }

    private static void encodeLong(final byte[] buf,final int offset,final int value){
        buf[offset + 0] = (byte) (value & 0xff);
        buf[offset + 1] = (byte) (value >> 8 & 0xff);
        buf[offset + 2] = (byte) (value >> 16 & 0xff);
        buf[offset + 3] = (byte) (value >> 24 & 0xff);
    }

    /**
     * Creates the NTLMv2 blob from the given target information block and
     * client challenge.
     *
     * @param targetInformation
     *            The target information block from the Type 2 message.
     * @param clientChallenge
     *            The random 8-byte client challenge.
     *
     * @return The blob, used in the calculation of the NTLMv2 Response.
     */
    private static byte[] createBlob(final byte[] clientChallenge,final byte[] targetInformation,final byte[] timestamp){
        final byte[] blobSignature = new byte[] { (byte) 0x01, (byte) 0x01, (byte) 0x00, (byte) 0x00 };
        final byte[] reserved = new byte[] { (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00 };
        final byte[] unknown1 = new byte[] { (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00 };
        final byte[] unknown2 = new byte[] { (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00 };
        final byte[] blob = new byte[blobSignature.length + reserved.length + timestamp.length + 8 + unknown1.length
                        + targetInformation.length + unknown2.length];
        int offset = 0;
        System.arraycopy(blobSignature, 0, blob, offset, blobSignature.length);
        offset += blobSignature.length;
        System.arraycopy(reserved, 0, blob, offset, reserved.length);
        offset += reserved.length;
        System.arraycopy(timestamp, 0, blob, offset, timestamp.length);
        offset += timestamp.length;
        System.arraycopy(clientChallenge, 0, blob, offset, 8);
        offset += 8;
        System.arraycopy(unknown1, 0, blob, offset, unknown1.length);
        offset += unknown1.length;
        System.arraycopy(targetInformation, 0, blob, offset, targetInformation.length);
        offset += targetInformation.length;
        System.arraycopy(unknown2, 0, blob, offset, unknown2.length);
        offset += unknown2.length;
        return blob;
    }

    /**
     * Creates a DES encryption key from the given key material.
     *
     * @param bytes
     *            A byte array containing the DES key material.
     * @param offset
     *            The offset in the given byte array at which the 7-byte key
     *            material starts.
     *
     * @return A DES encryption key created from the key material starting at
     *         the specified offset in the given byte array.
     */
    private static Key createDESKey(final byte[] bytes,final int offset){
        final byte[] keyBytes = new byte[7];
        System.arraycopy(bytes, offset, keyBytes, 0, 7);
        final byte[] material = new byte[8];
        material[0] = keyBytes[0];
        material[1] = (byte) (keyBytes[0] << 7 | (keyBytes[1] & 0xff) >>> 1);
        material[2] = (byte) (keyBytes[1] << 6 | (keyBytes[2] & 0xff) >>> 2);
        material[3] = (byte) (keyBytes[2] << 5 | (keyBytes[3] & 0xff) >>> 3);
        material[4] = (byte) (keyBytes[3] << 4 | (keyBytes[4] & 0xff) >>> 4);
        material[5] = (byte) (keyBytes[4] << 3 | (keyBytes[5] & 0xff) >>> 5);
        material[6] = (byte) (keyBytes[5] << 2 | (keyBytes[6] & 0xff) >>> 6);
        material[7] = (byte) (keyBytes[6] << 1);
        oddParity(material);
        return new SecretKeySpec(material, "DES");
    }

    /**
     * Applies odd parity to the given byte array.
     *
     * @param bytes
     *            The data whose parity bits are to be adjusted for odd parity.
     */
    private static void oddParity(final byte[] bytes){
        for (int i = 0; i < bytes.length; i++){
            final byte b = bytes[i];
            final boolean needsParity = (((b >>> 7) ^ (b >>> 6) ^ (b >>> 5) ^ (b >>> 4) ^ (b >>> 3) ^ (b >>> 2) ^ (b >>> 1)) & 0x01) == 0;
            if (needsParity){
                bytes[i] |= (byte) 0x01;
            }else{
                bytes[i] &= (byte) 0xfe;
            }
        }
    }

    /**
     * Find the character set based on the flags.
     * 
     * @param flags
     *            is the flags.
     * @return the character set.
     */
    private static Charset getCharset(final int flags) throws NTLMEngineException{
        if ((flags & FLAG_REQUEST_UNICODE_ENCODING) == 0){
            return DEFAULT_CHARSET;
        }
        if (UNICODE_LITTLE_UNMARKED == null){
            throw new NTLMEngineException("Unicode not supported");
        }
        return UNICODE_LITTLE_UNMARKED;
    }

    /** Strip dot suffix from a name */
    private static String stripDotSuffix(final String value){
        if (value == null){
            return null;
        }
        final int index = value.indexOf('.');
        if (index != -1){
            return value.substring(0, index);
        }
        return value;
    }

    /** Convert host to standard form */
    private static String convertHost(final String host){
        return stripDotSuffix(host);
    }

    /** Convert domain to standard form */
    private static String convertDomain(final String domain){
        return stripDotSuffix(domain);
    }

    /** NTLM message generation, base class */
    static class NTLMMessage{

        /** The current response */
        protected byte[] messageContents       = null;

        /** The current output position */
        protected int    currentOutputPosition = 0;

        /** Constructor to use when message contents are not yet known */
        NTLMMessage(){
        }

        /** Constructor taking a string */
        NTLMMessage(final String messageBody, final int expectedType) throws NTLMEngineException{
            this(Base64.decodeBase64(messageBody.getBytes(DEFAULT_CHARSET)), expectedType);
        }

        /** Constructor to use when message bytes are known */
        NTLMMessage(final byte[] message, final int expectedType) throws NTLMEngineException{
            messageContents = message;
            // Look for NTLM message
            if (messageContents.length < SIGNATURE.length){
                throw new NTLMEngineException("NTLM message decoding error - packet too short");
            }
            int i = 0;
            while (i < SIGNATURE.length){
                if (messageContents[i] != SIGNATURE[i]){
                    throw new NTLMEngineException("NTLM message expected - instead got unrecognized bytes");
                }
                i++;
            }

            // Check to be sure there's a type 2 message indicator next
            final int type = readULong(SIGNATURE.length);
            if (type != expectedType){
                throw new NTLMEngineException(
                                "NTLM type " + Integer.toString(expectedType) + " message expected - instead got type "
                                                + Integer.toString(type));
            }

            currentOutputPosition = messageContents.length;
        }

        /**
         * Get the length of the signature and flags, so calculations can adjust
         * offsets accordingly.
         */
        protected int getPreambleLength(){
            return SIGNATURE.length + 4;
        }

        /** Get the message length */
        protected int getMessageLength(){
            return currentOutputPosition;
        }

        /** Read a byte from a position within the message buffer */
        protected byte readByte(final int position) throws NTLMEngineException{
            if (messageContents.length < position + 1){
                throw new NTLMEngineException("NTLM: Message too short");
            }
            return messageContents[position];
        }

        /** Read a bunch of bytes from a position in the message buffer */
        protected void readBytes(final byte[] buffer,final int position) throws NTLMEngineException{
            if (messageContents.length < position + buffer.length){
                throw new NTLMEngineException("NTLM: Message too short");
            }
            System.arraycopy(messageContents, position, buffer, 0, buffer.length);
        }

        /** Read a ushort from a position within the message buffer */
        protected int readUShort(final int position) throws NTLMEngineException{
            return NTLMEngineImpl.readUShort(messageContents, position);
        }

        /** Read a ulong from a position within the message buffer */
        protected int readULong(final int position) throws NTLMEngineException{
            return NTLMEngineImpl.readULong(messageContents, position);
        }

        /** Read a security buffer from a position within the message buffer */
        protected byte[] readSecurityBuffer(final int position) throws NTLMEngineException{
            return NTLMEngineImpl.readSecurityBuffer(messageContents, position);
        }

        /**
         * Prepares the object to create a response of the given length.
         *
         * @param maxlength
         *            the maximum length of the response to prepare,
         *            including the type and the signature (which this method
         *            adds).
         */
        protected void prepareResponse(final int maxlength,final int messageType){
            messageContents = new byte[maxlength];
            currentOutputPosition = 0;
            addBytes(SIGNATURE);
            addULong(messageType);
        }

        /**
         * Adds the given byte to the response.
         *
         * @param b
         *            the byte to add.
         */
        protected void addByte(final byte b){
            messageContents[currentOutputPosition] = b;
            currentOutputPosition++;
        }

        /**
         * Adds the given bytes to the response.
         *
         * @param bytes
         *            the bytes to add.
         */
        protected void addBytes(final byte[] bytes){
            if (bytes == null){
                return;
            }
            for (final byte b : bytes){
                messageContents[currentOutputPosition] = b;
                currentOutputPosition++;
            }
        }

        /** Adds a USHORT to the response */
        protected void addUShort(final int value){
            addByte((byte) (value & 0xff));
            addByte((byte) (value >> 8 & 0xff));
        }

        /** Adds a ULong to the response */
        protected void addULong(final int value){
            addByte((byte) (value & 0xff));
            addByte((byte) (value >> 8 & 0xff));
            addByte((byte) (value >> 16 & 0xff));
            addByte((byte) (value >> 24 & 0xff));
        }

        /**
         * Returns the response that has been generated after shrinking the
         * array if required and base64 encodes the response.
         *
         * @return The response as above.
         */
        public String getResponse(){
            return new String(Base64.encodeBase64(getBytes()), Consts.ASCII);
        }

        public byte[] getBytes(){
            if (messageContents == null){
                buildMessage();
            }
            final byte[] resp;
            if (messageContents.length > currentOutputPosition){
                final byte[] tmp = new byte[currentOutputPosition];
                System.arraycopy(messageContents, 0, tmp, 0, currentOutputPosition);
                messageContents = tmp;
            }
            return messageContents;
        }

        protected void buildMessage(){
            throw new RuntimeException("Message builder not implemented for " + getClass().getName());
        }
    }

    /** Type 1 message assembly class */
    static class Type1Message extends NTLMMessage{

        private final byte[] hostBytes;

        private final byte[] domainBytes;

        private final int    flags;

        Type1Message(final String domain, final String host) throws NTLMEngineException{
            this(domain, host, null);
        }

        Type1Message(final String domain, final String host, final Integer flags) throws NTLMEngineException{
            super();
            this.flags = ((flags == null) ? getDefaultFlags() : flags);

            // Strip off domain name from the host!
            final String unqualifiedHost = convertHost(host);
            // Use only the base domain name!
            final String unqualifiedDomain = convertDomain(domain);

            hostBytes = unqualifiedHost != null ? unqualifiedHost.getBytes(UNICODE_LITTLE_UNMARKED) : null;
            domainBytes = unqualifiedDomain != null ? unqualifiedDomain.toUpperCase(Locale.ROOT).getBytes(UNICODE_LITTLE_UNMARKED) : null;
        }

        Type1Message(){
            super();
            hostBytes = null;
            domainBytes = null;
            flags = getDefaultFlags();
        }

        private int getDefaultFlags(){
            return
            //FLAG_WORKSTATION_PRESENT |
            //FLAG_DOMAIN_PRESENT |

            // Required flags
            //FLAG_REQUEST_LAN_MANAGER_KEY |
            FLAG_REQUEST_NTLMv1 | FLAG_REQUEST_NTLM2_SESSION |

            // Protocol version request
                            FLAG_REQUEST_VERSION |

                            // Recommended privacy settings
                            FLAG_REQUEST_ALWAYS_SIGN |
                            //FLAG_REQUEST_SEAL |
                            //FLAG_REQUEST_SIGN |

                            // These must be set according to documentation, based on use of SEAL above
                            FLAG_REQUEST_128BIT_KEY_EXCH | FLAG_REQUEST_56BIT_ENCRYPTION |
                            //FLAG_REQUEST_EXPLICIT_KEY_EXCH |

                            FLAG_REQUEST_UNICODE_ENCODING;

        }

        /**
         * Getting the response involves building the message before returning
         * it
         */
        @Override
        protected void buildMessage(){
            int domainBytesLength = 0;
            if (domainBytes != null){
                domainBytesLength = domainBytes.length;
            }
            int hostBytesLength = 0;
            if (hostBytes != null){
                hostBytesLength = hostBytes.length;
            }

            // Now, build the message. Calculate its length first, including
            // signature or type.
            final int finalLength = 32 + 8 + hostBytesLength + domainBytesLength;

            // Set up the response. This will initialize the signature, message
            // type, and flags.
            prepareResponse(finalLength, 1);

            // Flags. These are the complete set of flags we support.
            addULong(flags);

            // Domain length (two times).
            addUShort(domainBytesLength);
            addUShort(domainBytesLength);

            // Domain offset.
            addULong(hostBytesLength + 32 + 8);

            // Host length (two times).
            addUShort(hostBytesLength);
            addUShort(hostBytesLength);

            // Host offset (always 32 + 8).
            addULong(32 + 8);

            // Version
            addUShort(0x0105);
            // Build
            addULong(2600);
            // NTLM revision
            addUShort(0x0f00);

            // Host (workstation) String.
            if (hostBytes != null){
                addBytes(hostBytes);
            }
            // Domain String.
            if (domainBytes != null){
                addBytes(domainBytes);
            }
        }

    }

    /** Type 2 message class */
    static class Type2Message extends NTLMMessage{

        protected final byte[] challenge;

        protected String       target;

        protected byte[]       targetInfo;

        protected final int    flags;

        Type2Message(final String messageBody) throws NTLMEngineException{
            this(Base64.decodeBase64(messageBody.getBytes(DEFAULT_CHARSET)));
        }

        Type2Message(final byte[] message) throws NTLMEngineException{
            super(message, 2);

            // Type 2 message is laid out as follows:
            // First 8 bytes: NTLMSSP[0]
            // Next 4 bytes: Ulong, value 2
            // Next 8 bytes, starting at offset 12: target field (2 ushort lengths, 1 ulong offset)
            // Next 4 bytes, starting at offset 20: Flags, e.g. 0x22890235
            // Next 8 bytes, starting at offset 24: Challenge
            // Next 8 bytes, starting at offset 32: ??? (8 bytes of zeros)
            // Next 8 bytes, starting at offset 40: targetinfo field (2 ushort lengths, 1 ulong offset)
            // Next 2 bytes, major/minor version number (e.g. 0x05 0x02)
            // Next 8 bytes, build number
            // Next 2 bytes, protocol version number (e.g. 0x00 0x0f)
            // Next, various text fields, and a ushort of value 0 at the end

            // Parse out the rest of the info we need from the message
            // The nonce is the 8 bytes starting from the byte in position 24.
            challenge = new byte[8];
            readBytes(challenge, 24);

            flags = readULong(20);

            // Do the target!
            target = null;
            // The TARGET_DESIRED flag is said to not have understood semantics
            // in Type2 messages, so use the length of the packet to decide
            // how to proceed instead
            if (getMessageLength() >= 12 + 8){
                final byte[] bytes = readSecurityBuffer(12);
                if (bytes.length != 0){
                    target = new String(bytes, getCharset(flags));
                }
            }

            // Do the target info!
            targetInfo = null;
            // TARGET_DESIRED flag cannot be relied on, so use packet length
            if (getMessageLength() >= 40 + 8){
                final byte[] bytes = readSecurityBuffer(40);
                if (bytes.length != 0){
                    targetInfo = bytes;
                }
            }
        }

        /** Retrieve the challenge */
        byte[] getChallenge(){
            return challenge;
        }

        /** Retrieve the target */
        String getTarget(){
            return target;
        }

        /** Retrieve the target info */
        byte[] getTargetInfo(){
            return targetInfo;
        }

        /** Retrieve the response flags */
        int getFlags(){
            return flags;
        }

    }

    /** Type 3 message assembly class */
    static class Type3Message extends NTLMMessage{

        // For mic computation
        protected final byte[]  type1Message;

        protected final byte[]  type2Message;

        // Response flags from the type2 message
        protected final int     type2Flags;

        protected final byte[]  domainBytes;

        protected final byte[]  hostBytes;

        protected final byte[]  userBytes;

        protected byte[]        lmResp;

        protected byte[]        ntResp;

        protected final byte[]  sessionKey;

        protected final byte[]  exportedSessionKey;

        protected final boolean computeMic;

        /**
         * More primitive constructor: don't include cert or previous messages.
         */
        Type3Message(final String domain, final String host, final String user, final String password, final byte[] nonce,
                        final int type2Flags, final String target, final byte[] targetInformation) throws NTLMEngineException{
            this(domain, host, user, password, nonce, type2Flags, target, targetInformation, null, null, null);
        }

        /**
         * More primitive constructor: don't include cert or previous messages.
         */
        Type3Message(final Random random, final long currentTime, final String domain, final String host, final String user,
                        final String password, final byte[] nonce, final int type2Flags, final String target,
                        final byte[] targetInformation) throws NTLMEngineException{
            this(random, currentTime, domain, host, user, password, nonce, type2Flags, target, targetInformation, null, null, null);
        }

        /** Constructor. Pass the arguments we will need */
        Type3Message(final String domain, final String host, final String user, final String password, final byte[] nonce,
                        final int type2Flags, final String target, final byte[] targetInformation, final Certificate peerServerCertificate,
                        final byte[] type1Message, final byte[] type2Message) throws NTLMEngineException{
            this(RND_GEN, System.currentTimeMillis(), domain, host, user, password, nonce, type2Flags, target, targetInformation,
                            peerServerCertificate, type1Message, type2Message);
        }

        /** Constructor. Pass the arguments we will need */
        Type3Message(final Random random, final long currentTime, final String domain, final String host, final String user,
                        final String password, final byte[] nonce, final int type2Flags, final String target,
                        final byte[] targetInformation, final Certificate peerServerCertificate, final byte[] type1Message,
                        final byte[] type2Message) throws NTLMEngineException{

            if (random == null){
                throw new NTLMEngineException("Random generator not available");
            }

            // Save the flags
            this.type2Flags = type2Flags;
            this.type1Message = type1Message;
            this.type2Message = type2Message;

            // Strip off domain name from the host!
            final String unqualifiedHost = convertHost(host);
            // Use only the base domain name!
            final String unqualifiedDomain = convertDomain(domain);

            byte[] responseTargetInformation = targetInformation;
            if (peerServerCertificate != null){
                responseTargetInformation = addGssMicAvsToTargetInfo(targetInformation, peerServerCertificate);
                computeMic = true;
            }else{
                computeMic = false;
            }

            // Create a cipher generator class.  Use domain BEFORE it gets modified!
            final CipherGen gen = new CipherGen(
                            random,
                            currentTime,
                            unqualifiedDomain,
                            user,
                            password,
                            nonce,
                            target,
                            responseTargetInformation);

            // Use the new code to calculate the responses, including v2 if that
            // seems warranted.
            byte[] userSessionKey;
            try{
                // This conditional may not work on Windows Server 2008 R2 and above, where it has not yet
                // been tested
                if (((type2Flags & FLAG_TARGETINFO_PRESENT) != 0) && targetInformation != null && target != null){
                    // NTLMv2
                    ntResp = gen.getNTLMv2Response();
                    lmResp = gen.getLMv2Response();
                    if ((type2Flags & FLAG_REQUEST_LAN_MANAGER_KEY) != 0){
                        userSessionKey = gen.getLanManagerSessionKey();
                    }else{
                        userSessionKey = gen.getNTLMv2UserSessionKey();
                    }
                }else{
                    // NTLMv1
                    if ((type2Flags & FLAG_REQUEST_NTLM2_SESSION) != 0){
                        // NTLM2 session stuff is requested
                        ntResp = gen.getNTLM2SessionResponse();
                        lmResp = gen.getLM2SessionResponse();
                        if ((type2Flags & FLAG_REQUEST_LAN_MANAGER_KEY) != 0){
                            userSessionKey = gen.getLanManagerSessionKey();
                        }else{
                            userSessionKey = gen.getNTLM2SessionResponseUserSessionKey();
                        }
                    }else{
                        ntResp = gen.getNTLMResponse();
                        lmResp = gen.getLMResponse();
                        if ((type2Flags & FLAG_REQUEST_LAN_MANAGER_KEY) != 0){
                            userSessionKey = gen.getLanManagerSessionKey();
                        }else{
                            userSessionKey = gen.getNTLMUserSessionKey();
                        }
                    }
                }
            }catch (final NTLMEngineException e){
                // This likely means we couldn't find the MD4 hash algorithm -
                // fail back to just using LM
                ntResp = new byte[0];
                lmResp = gen.getLMResponse();
                if ((type2Flags & FLAG_REQUEST_LAN_MANAGER_KEY) != 0){
                    userSessionKey = gen.getLanManagerSessionKey();
                }else{
                    userSessionKey = gen.getLMUserSessionKey();
                }
            }

            if ((type2Flags & FLAG_REQUEST_SIGN) != 0){
                if ((type2Flags & FLAG_REQUEST_EXPLICIT_KEY_EXCH) != 0){
                    exportedSessionKey = gen.getSecondaryKey();
                    sessionKey = RC4(exportedSessionKey, userSessionKey);
                }else{
                    sessionKey = userSessionKey;
                    exportedSessionKey = sessionKey;
                }
            }else{
                if (computeMic){
                    throw new NTLMEngineException("Cannot sign/seal: no exported session key");
                }
                sessionKey = null;
                exportedSessionKey = null;
            }
            final Charset charset = getCharset(type2Flags);
            hostBytes = unqualifiedHost != null ? unqualifiedHost.getBytes(charset) : null;
            domainBytes = unqualifiedDomain != null ? unqualifiedDomain.toUpperCase(Locale.ROOT).getBytes(charset) : null;
            userBytes = user.getBytes(charset);
        }

        public byte[] getEncryptedRandomSessionKey(){
            return sessionKey;
        }

        public byte[] getExportedSessionKey(){
            return exportedSessionKey;
        }

        /** Assemble the response */
        @Override
        protected void buildMessage(){
            final int ntRespLen = ntResp.length;
            final int lmRespLen = lmResp.length;

            final int domainLen = domainBytes != null ? domainBytes.length : 0;
            final int hostLen = hostBytes != null ? hostBytes.length : 0;
            final int userLen = userBytes.length;
            final int sessionKeyLen;
            if (sessionKey != null){
                sessionKeyLen = sessionKey.length;
            }else{
                sessionKeyLen = 0;
            }

            // Calculate the layout within the packet
            final int lmRespOffset = 72 + // allocate space for the version
                            (computeMic ? 16 : 0); // and MIC
            final int ntRespOffset = lmRespOffset + lmRespLen;
            final int domainOffset = ntRespOffset + ntRespLen;
            final int userOffset = domainOffset + domainLen;
            final int hostOffset = userOffset + userLen;
            final int sessionKeyOffset = hostOffset + hostLen;
            final int finalLength = sessionKeyOffset + sessionKeyLen;

            // Start the response. Length includes signature and type
            prepareResponse(finalLength, 3);

            // LM Resp Length (twice)
            addUShort(lmRespLen);
            addUShort(lmRespLen);

            // LM Resp Offset
            addULong(lmRespOffset);

            // NT Resp Length (twice)
            addUShort(ntRespLen);
            addUShort(ntRespLen);

            // NT Resp Offset
            addULong(ntRespOffset);

            // Domain length (twice)
            addUShort(domainLen);
            addUShort(domainLen);

            // Domain offset.
            addULong(domainOffset);

            // User Length (twice)
            addUShort(userLen);
            addUShort(userLen);

            // User offset
            addULong(userOffset);

            // Host length (twice)
            addUShort(hostLen);
            addUShort(hostLen);

            // Host offset
            addULong(hostOffset);

            // Session key length (twice)
            addUShort(sessionKeyLen);
            addUShort(sessionKeyLen);

            // Session key offset
            addULong(sessionKeyOffset);

            // Flags.
            addULong(
                            /*
                             * //FLAG_WORKSTATION_PRESENT |
                             * //FLAG_DOMAIN_PRESENT |
                             * 
                             * // Required flags
                             * (type2Flags & FLAG_REQUEST_LAN_MANAGER_KEY) |
                             * (type2Flags & FLAG_REQUEST_NTLMv1) |
                             * (type2Flags & FLAG_REQUEST_NTLM2_SESSION) |
                             * 
                             * // Protocol version request
                             * FLAG_REQUEST_VERSION |
                             * 
                             * // Recommended privacy settings
                             * (type2Flags & FLAG_REQUEST_ALWAYS_SIGN) |
                             * (type2Flags & FLAG_REQUEST_SEAL) |
                             * (type2Flags & FLAG_REQUEST_SIGN) |
                             * 
                             * // These must be set according to documentation, based on use of SEAL above
                             * (type2Flags & FLAG_REQUEST_128BIT_KEY_EXCH) |
                             * (type2Flags & FLAG_REQUEST_56BIT_ENCRYPTION) |
                             * (type2Flags & FLAG_REQUEST_EXPLICIT_KEY_EXCH) |
                             * 
                             * (type2Flags & FLAG_TARGETINFO_PRESENT) |
                             * (type2Flags & FLAG_REQUEST_UNICODE_ENCODING) |
                             * (type2Flags & FLAG_REQUEST_TARGET)
                             */
                            type2Flags);

            // Version
            addUShort(0x0105);
            // Build
            addULong(2600);
            // NTLM revision
            addUShort(0x0f00);

            int micPosition = -1;
            if (computeMic){
                micPosition = currentOutputPosition;
                currentOutputPosition += 16;
            }

            // Add the actual data
            addBytes(lmResp);
            addBytes(ntResp);
            addBytes(domainBytes);
            addBytes(userBytes);
            addBytes(hostBytes);
            if (sessionKey != null){
                addBytes(sessionKey);
            }

            // Write the mic back into its slot in the message

            if (computeMic){
                // Computation of message integrity code (MIC) as specified in [MS-NLMP] section 3.2.5.1.2.
                final HMACMD5 hmacMD5 = new HMACMD5(exportedSessionKey);
                hmacMD5.update(type1Message);
                hmacMD5.update(type2Message);
                hmacMD5.update(messageContents);
                final byte[] mic = hmacMD5.getOutput();
                System.arraycopy(mic, 0, messageContents, micPosition, mic.length);
            }
        }

        /**
         * Add GSS channel binding hash and MIC flag to the targetInfo.
         * Looks like this is needed if we want to use exported session key for GSS wrapping.
         */
        private byte[] addGssMicAvsToTargetInfo(final byte[] originalTargetInfo,final Certificate peerServerCertificate)
                        throws NTLMEngineException{
            final byte[] newTargetInfo = new byte[originalTargetInfo.length + 8 + 20];
            final int appendLength = originalTargetInfo.length - 4; // last tag is MSV_AV_EOL, do not copy that
            System.arraycopy(originalTargetInfo, 0, newTargetInfo, 0, appendLength);
            writeUShort(newTargetInfo, MSV_AV_FLAGS, appendLength);
            writeUShort(newTargetInfo, 4, appendLength + 2);
            writeULong(newTargetInfo, MSV_AV_FLAGS_MIC, appendLength + 4);
            writeUShort(newTargetInfo, MSV_AV_CHANNEL_BINDINGS, appendLength + 8);
            writeUShort(newTargetInfo, 16, appendLength + 10);

            final byte[] channelBindingsHash;
            try{
                final byte[] certBytes = peerServerCertificate.getEncoded();
                final MessageDigest sha256 = MessageDigest.getInstance("SHA-256");
                final byte[] certHashBytes = sha256.digest(certBytes);
                final byte[] channelBindingStruct = new byte[16 + 4 + MAGIC_TLS_SERVER_ENDPOINT.length + certHashBytes.length];
                writeULong(channelBindingStruct, 0x00000035, 16);
                System.arraycopy(MAGIC_TLS_SERVER_ENDPOINT, 0, channelBindingStruct, 20, MAGIC_TLS_SERVER_ENDPOINT.length);
                System.arraycopy(certHashBytes, 0, channelBindingStruct, 20 + MAGIC_TLS_SERVER_ENDPOINT.length, certHashBytes.length);
                final MessageDigest md5 = getMD5();
                channelBindingsHash = md5.digest(channelBindingStruct);
            }catch (final CertificateEncodingException e){
                throw new NTLMEngineException(e.getMessage(), e);
            }catch (final NoSuchAlgorithmException e){
                throw new NTLMEngineException(e.getMessage(), e);
            }

            System.arraycopy(channelBindingsHash, 0, newTargetInfo, appendLength + 12, 16);
            return newTargetInfo;
        }

    }

    static void writeUShort(final byte[] buffer,final int value,final int offset){
        buffer[offset] = (byte) (value & 0xff);
        buffer[offset + 1] = (byte) (value >> 8 & 0xff);
    }

    static void writeULong(final byte[] buffer,final int value,final int offset){
        buffer[offset] = (byte) (value & 0xff);
        buffer[offset + 1] = (byte) (value >> 8 & 0xff);
        buffer[offset + 2] = (byte) (value >> 16 & 0xff);
        buffer[offset + 3] = (byte) (value >> 24 & 0xff);
    }

    static int F(final int x,final int y,final int z){
        return ((x & y) | (~x & z));
    }

    static int G(final int x,final int y,final int z){
        return ((x & y) | (x & z) | (y & z));
    }

    static int H(final int x,final int y,final int z){
        return (x ^ y ^ z);
    }

    static int rotintlft(final int val,final int numbits){
        return ((val << numbits) | (val >>> (32 - numbits)));
    }

    static MessageDigest getMD5(){
        try{
            return MessageDigest.getInstance("MD5");
        }catch (final NoSuchAlgorithmException ex){
            throw new RuntimeException("MD5 message digest doesn't seem to exist - fatal error: " + ex.getMessage(), ex);
        }
    }

    /**
     * Cryptography support - MD4. The following class was based loosely on the
     * RFC and on code found at http://www.cs.umd.edu/~harry/jotp/src/md.java.
     * Code correctness was verified by looking at MD4.java from the jcifs
     * library (http://jcifs.samba.org). It was massaged extensively to the
     * final form found here by Karl Wright (kwright@metacarta.com).
     */
    static class MD4{

        protected int          A          = 0x67452301;

        protected int          B          = 0xefcdab89;

        protected int          C          = 0x98badcfe;

        protected int          D          = 0x10325476;

        protected long         count      = 0L;

        protected final byte[] dataBuffer = new byte[64];

        MD4(){
        }

        void update(final byte[] input){
            // We always deal with 512 bits at a time. Correspondingly, there is
            // a buffer 64 bytes long that we write data into until it gets
            // full.
            int curBufferPos = (int) (count & 63L);
            int inputIndex = 0;
            while (input.length - inputIndex + curBufferPos >= dataBuffer.length){
                // We have enough data to do the next step. Do a partial copy
                // and a transform, updating inputIndex and curBufferPos
                // accordingly
                final int transferAmt = dataBuffer.length - curBufferPos;
                System.arraycopy(input, inputIndex, dataBuffer, curBufferPos, transferAmt);
                count += transferAmt;
                curBufferPos = 0;
                inputIndex += transferAmt;
                processBuffer();
            }

            // If there's anything left, copy it into the buffer and leave it.
            // We know there's not enough left to process.
            if (inputIndex < input.length){
                final int transferAmt = input.length - inputIndex;
                System.arraycopy(input, inputIndex, dataBuffer, curBufferPos, transferAmt);
                count += transferAmt;
                curBufferPos += transferAmt;
            }
        }

        byte[] getOutput(){
            // Feed pad/length data into engine. This must round out the input
            // to a multiple of 512 bits.
            final int bufferIndex = (int) (count & 63L);
            final int padLen = (bufferIndex < 56) ? (56 - bufferIndex) : (120 - bufferIndex);
            final byte[] postBytes = new byte[padLen + 8];
            // Leading 0x80, specified amount of zero padding, then length in
            // bits.
            postBytes[0] = (byte) 0x80;
            // Fill out the last 8 bytes with the length
            for (int i = 0; i < 8; i++){
                postBytes[padLen + i] = (byte) ((count * 8) >>> (8 * i));
            }

            // Update the engine
            update(postBytes);

            // Calculate final result
            final byte[] result = new byte[16];
            writeULong(result, A, 0);
            writeULong(result, B, 4);
            writeULong(result, C, 8);
            writeULong(result, D, 12);
            return result;
        }

        protected void processBuffer(){
            // Convert current buffer to 16 ulongs
            final int[] d = new int[16];

            for (int i = 0; i < 16; i++){
                d[i] = (dataBuffer[i * 4] & 0xff) + ((dataBuffer[i * 4 + 1] & 0xff) << 8) + ((dataBuffer[i * 4 + 2] & 0xff) << 16)
                                + ((dataBuffer[i * 4 + 3] & 0xff) << 24);
            }

            // Do a round of processing
            final int AA = A;
            final int BB = B;
            final int CC = C;
            final int DD = D;
            round1(d);
            round2(d);
            round3(d);
            A += AA;
            B += BB;
            C += CC;
            D += DD;

        }

        protected void round1(final int[] d){
            A = rotintlft((A + F(B, C, D) + d[0]), 3);
            D = rotintlft((D + F(A, B, C) + d[1]), 7);
            C = rotintlft((C + F(D, A, B) + d[2]), 11);
            B = rotintlft((B + F(C, D, A) + d[3]), 19);

            A = rotintlft((A + F(B, C, D) + d[4]), 3);
            D = rotintlft((D + F(A, B, C) + d[5]), 7);
            C = rotintlft((C + F(D, A, B) + d[6]), 11);
            B = rotintlft((B + F(C, D, A) + d[7]), 19);

            A = rotintlft((A + F(B, C, D) + d[8]), 3);
            D = rotintlft((D + F(A, B, C) + d[9]), 7);
            C = rotintlft((C + F(D, A, B) + d[10]), 11);
            B = rotintlft((B + F(C, D, A) + d[11]), 19);

            A = rotintlft((A + F(B, C, D) + d[12]), 3);
            D = rotintlft((D + F(A, B, C) + d[13]), 7);
            C = rotintlft((C + F(D, A, B) + d[14]), 11);
            B = rotintlft((B + F(C, D, A) + d[15]), 19);
        }

        protected void round2(final int[] d){
            A = rotintlft((A + G(B, C, D) + d[0] + 0x5a827999), 3);
            D = rotintlft((D + G(A, B, C) + d[4] + 0x5a827999), 5);
            C = rotintlft((C + G(D, A, B) + d[8] + 0x5a827999), 9);
            B = rotintlft((B + G(C, D, A) + d[12] + 0x5a827999), 13);

            A = rotintlft((A + G(B, C, D) + d[1] + 0x5a827999), 3);
            D = rotintlft((D + G(A, B, C) + d[5] + 0x5a827999), 5);
            C = rotintlft((C + G(D, A, B) + d[9] + 0x5a827999), 9);
            B = rotintlft((B + G(C, D, A) + d[13] + 0x5a827999), 13);

            A = rotintlft((A + G(B, C, D) + d[2] + 0x5a827999), 3);
            D = rotintlft((D + G(A, B, C) + d[6] + 0x5a827999), 5);
            C = rotintlft((C + G(D, A, B) + d[10] + 0x5a827999), 9);
            B = rotintlft((B + G(C, D, A) + d[14] + 0x5a827999), 13);

            A = rotintlft((A + G(B, C, D) + d[3] + 0x5a827999), 3);
            D = rotintlft((D + G(A, B, C) + d[7] + 0x5a827999), 5);
            C = rotintlft((C + G(D, A, B) + d[11] + 0x5a827999), 9);
            B = rotintlft((B + G(C, D, A) + d[15] + 0x5a827999), 13);

        }

        protected void round3(final int[] d){
            A = rotintlft((A + H(B, C, D) + d[0] + 0x6ed9eba1), 3);
            D = rotintlft((D + H(A, B, C) + d[8] + 0x6ed9eba1), 9);
            C = rotintlft((C + H(D, A, B) + d[4] + 0x6ed9eba1), 11);
            B = rotintlft((B + H(C, D, A) + d[12] + 0x6ed9eba1), 15);

            A = rotintlft((A + H(B, C, D) + d[2] + 0x6ed9eba1), 3);
            D = rotintlft((D + H(A, B, C) + d[10] + 0x6ed9eba1), 9);
            C = rotintlft((C + H(D, A, B) + d[6] + 0x6ed9eba1), 11);
            B = rotintlft((B + H(C, D, A) + d[14] + 0x6ed9eba1), 15);

            A = rotintlft((A + H(B, C, D) + d[1] + 0x6ed9eba1), 3);
            D = rotintlft((D + H(A, B, C) + d[9] + 0x6ed9eba1), 9);
            C = rotintlft((C + H(D, A, B) + d[5] + 0x6ed9eba1), 11);
            B = rotintlft((B + H(C, D, A) + d[13] + 0x6ed9eba1), 15);

            A = rotintlft((A + H(B, C, D) + d[3] + 0x6ed9eba1), 3);
            D = rotintlft((D + H(A, B, C) + d[11] + 0x6ed9eba1), 9);
            C = rotintlft((C + H(D, A, B) + d[7] + 0x6ed9eba1), 11);
            B = rotintlft((B + H(C, D, A) + d[15] + 0x6ed9eba1), 15);

        }

    }

    /**
     * Cryptography support - HMACMD5 - algorithmically based on various web
     * resources by Karl Wright
     */
    static class HMACMD5{

        protected final byte[]        ipad;

        protected final byte[]        opad;

        protected final MessageDigest md5;

        HMACMD5(final byte[] input){
            byte[] key = input;
            md5 = getMD5();

            // Initialize the pad buffers with the key
            ipad = new byte[64];
            opad = new byte[64];

            int keyLength = key.length;
            if (keyLength > 64){
                // Use MD5 of the key instead, as described in RFC 2104
                md5.update(key);
                key = md5.digest();
                keyLength = key.length;
            }
            int i = 0;
            while (i < keyLength){
                ipad[i] = (byte) (key[i] ^ (byte) 0x36);
                opad[i] = (byte) (key[i] ^ (byte) 0x5c);
                i++;
            }
            while (i < 64){
                ipad[i] = (byte) 0x36;
                opad[i] = (byte) 0x5c;
                i++;
            }

            // Very important: processChallenge the digest with the ipad buffer
            md5.reset();
            md5.update(ipad);

        }

        /** Grab the current digest. This is the "answer". */
        byte[] getOutput(){
            final byte[] digest = md5.digest();
            md5.update(opad);
            return md5.digest(digest);
        }

        /** Update by adding a complete array */
        void update(final byte[] input){
            md5.update(input);
        }

        /** Update the algorithm */
        void update(final byte[] input,final int offset,final int length){
            md5.update(input, offset, length);
        }

    }

    @Override
    public String generateType1Msg(final String domain,final String workstation) throws NTLMEngineException{
        return getType1Message(workstation, domain);
    }

    @Override
    public String generateType3Msg(
                    final String username,
                    final String password,
                    final String domain,
                    final String workstation,
                    final String challenge) throws NTLMEngineException{
        final Type2Message t2m = new Type2Message(challenge);
        return getType3Message(
                        username,
                        password,
                        workstation,
                        domain,
                        t2m.getChallenge(),
                        t2m.getFlags(),
                        t2m.getTarget(),
                        t2m.getTargetInfo());
    }

}
